﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace GeneticAlgorithm
{
    class Algorithm
    {
        /// <summary>
        /// Key is item, value is profit from item. 
        /// Sorted by descending.
        /// </summary>
        public Dictionary<Item, float> Profits = new Dictionary<Item, float>();
        /// <summary>
        /// User's relevance
        /// </summary>
        private readonly List<int> _relevance;

        private const int MinStock = 5;
        private const int MaxStock = 50;

        private int _iteration = 0;
        public Algorithm(List<int> relevance)
        {
            _relevance = relevance;
        }

        /// <summary>
        /// The main function
        /// </summary>
        /// <returns>The top-4 items which need to buy</returns>
        public List<Item> GetAdvice()
        {
            var first = Generator.GenerateFirstPopulation();
            FillProfitsDictionary(first);

            var generation = GetTop(50, first);

            while (true)
            {
                generation = GetNextGeneration(generation);
                if (IsBestGeneration(generation))
                {
                    break;
                }
            }
            const int number = 4;

            return GetTop(number, generation, true);
        }

        /// <summary>
        /// Creates new generation from old generation
        /// </summary>
        /// <param name="oldGeneration"></param>
        /// <returns>First 30 individuals from new generation</returns>
        private List<Item> GetNextGeneration(List<Item> oldGeneration)
        {
            var newGeneration = CreateCrossing(CreateMutation(oldGeneration));

            const int number = 30;
            return GetTop(number, newGeneration);
        }

        /// <summary>
        /// Returns true after 10 iterations
        /// </summary>
        /// <param name="generation"></param>
        /// <returns></returns>
        private bool IsBestGeneration(List<Item> generation)
        {
            return (_iteration++ > 10);
        }

        #region fitness-function methods
        /// <summary>
        /// This method returns the probability of purchase of the item by the user
        /// </summary>
        /// <param name="item"></param>
        /// <returns>the probability of purchase of the item by the user</returns>
        private float GetProbability(Item item)
        {
            var result = ((Generator.MaxPrice + 1) - item.Price) * _relevance[item.Category] / (float)Generator.MaxPrice;
            if (result >= MaxStock || result <= MinStock)
            {
                result *= 3;
            }
            return (result >= 1 ) ? 1 : result;
        }

        /// <summary>
        /// Our fitness-function.
        /// </summary>
        /// <param name="item">current item</param>
        /// <returns>profit from item</returns>
        private float GetProfit(Item item)
        {
            return item.Margin * GetProbability(item);
        }

        #endregion
        
        #region selection methods

        /// <summary>
        /// Analog of selection-method in genetic algorithm.
        /// </summary>
        /// <param name="number">Number of the best individuals</param>
        /// <param name="generation">Current generation</param>
        /// <returns>The first $number$ best individuals</returns>
        private List<Item> GetTop(int number, List<Item> generation, bool isFinished = false)
        {
            FillProfitsDictionary(generation);
            var result = Profits.Keys.Take(number).ToList();
            return result;
        }

        /// <summary>
        /// Fills the Profits dictionary by items from current generation and sorts it by descending.
        /// </summary>
        /// <param name="generation">current generation</param>
        private void FillProfitsDictionary(List<Item> generation)
        {
            Profits.Clear();
            foreach (var item in generation)
            {
                Profits.Add(item, GetProfit(item));
            }
            Profits = Profits.OrderByDescending(pair => pair.Value).ToDictionary(pair => pair.Key, pair => pair.Value);
        }

        #endregion
        
        #region mutation methods

        /// <summary>
        /// 
        /// </summary>
        /// <param name="generation"></param>
        /// <returns></returns>
        private List<Item> CreateMutation(List<Item> generation)
        {
            const int mutationNumber = 2;
            var count = 0;
            var random = new Random();

            while(count < mutationNumber)
            {
                var num = random.Next(generation.Count);

                var newItem = DoMutation(generation[num]);
                if (newItem != null)
                {
                    generation.Add(newItem);
                    count++;
                }
            }

            return generation;
        }

        private Item DoMutation(Item oldItem)
        {
            var item = new Item();

            var random = new Random();
            const int upperBound = 2;

            item.Price = random.Next(upperBound) == 1 ? random.Next(Generator.MaxPrice) : oldItem.Price;

            item.Margin = random.Next(upperBound) == 1 ? random.Next(Generator.MaxMargin) : oldItem.Margin;
            
            item.Count = random.Next(upperBound) == 1 ? random.Next(Generator.MaxCount) : oldItem.Count;

            item.Category = random.Next(upperBound) == 1 ? random.Next(Generator.CategoryCount) : oldItem.Category;

            item.Name = Generator.GenerateName();

            return item;
        }
        
        #endregion
        
        #region crossing methods
        
        private List<Item> CreateCrossing(List<Item> oldGeneration)
        {
            var newGeneration = new List<Item>();

            for (var i = 0; i < oldGeneration.Count; i++)
            {
                for (var j = i + 1; j < oldGeneration.Count; j++)
                {
                    var newItem = Cross(oldGeneration[i], oldGeneration[j]);
                    if (!newGeneration.Contains(newItem))
                    {
                        newGeneration.Add(newItem);
                    }
                    else
                    {
                        // only for debug
                    }
                }
            }
            
            return newGeneration;
        }

        
        /// <summary>
        /// This method crosses two items
        /// </summary>
        /// <param name="one">First happy parent</param>
        /// <param name="two">Second happy parent</param>
        /// <returns>Baby</returns>
        private Item Cross(Item one, Item two)
        {
            var newItem = new Item();
            var random = new Random();
            const int upperBound = 2;

            newItem.Price = random.Next(upperBound) == 0 ? one.Price : two.Price;
            
            newItem.Margin = random.Next(upperBound) == 0 ? one.Margin : two.Margin;

            newItem.Category = random.Next(upperBound) == 0 ? one.Category : two.Category;

            newItem.Count = random.Next(upperBound) == 0 ? one.Count : two.Count;

            newItem.Name = random.Next(upperBound) == 0 ? one.Name : two.Name;
            
            return newItem;
        }

        #endregion
    }
}
